/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.spring.initializr.web.project;

import io.spring.initializr.generator.buildsystem.BuildItemResolver;
import io.spring.initializr.generator.buildsystem.BuildWriter;
import io.spring.initializr.generator.project.*;
import io.spring.initializr.metadata.InitializrMetadata;
import io.spring.initializr.metadata.InitializrMetadataProvider;
import io.spring.initializr.metadata.support.MetadataBuildItemResolver;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.util.FileSystemUtils;

import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 调用项目生成API。这是一个中间层，它可以使用{@link ProjectRequest}并根据请求触发项目生成。 Invokes the project
 * generation API. This is an intermediate layer that can consume a {@link ProjectRequest}
 * and trigger project generation based on the request.
 *
 * @param <R> the concrete {@link ProjectRequest} type
 * @author Madhura Bhave
 */
public class ProjectGenerationInvoker<R extends ProjectRequest> {

	private final ApplicationContext parentApplicationContext;

	private final ApplicationEventPublisher eventPublisher;

	private final ProjectRequestToDescriptionConverter<R> requestConverter;

	private transient Map<Path, List<Path>> temporaryFiles = new LinkedHashMap<>();

	public ProjectGenerationInvoker(ApplicationContext parentApplicationContext,
			ProjectRequestToDescriptionConverter<R> requestConverter) {
		this(parentApplicationContext, parentApplicationContext, requestConverter);
	}

	protected ProjectGenerationInvoker(ApplicationContext parentApplicationContext,
			ApplicationEventPublisher eventPublisher, ProjectRequestToDescriptionConverter<R> requestConverter) {
		this.parentApplicationContext = parentApplicationContext;
		this.eventPublisher = eventPublisher;
		this.requestConverter = requestConverter;
	}

	/**
	 * 调用为指定的{@link ProjectRequest}生成整个项目结构的项目生成API。 Invokes the project generation API
	 * that generates the entire project structure for the specified
	 * {@link ProjectRequest}.
	 * @param request the project request
	 * @return the {@link ProjectGenerationResult}
	 */
	public ProjectGenerationResult invokeProjectStructureGeneration(R request) {

		// 从父容器中获取项目生成所需要的初始化元信息
		InitializrMetadata metadata = this.parentApplicationContext.getBean(InitializrMetadataProvider.class).get();
		try {
			// 项目描述信息，并验证request的数据是否与metadata匹配，不匹配直接报错
			ProjectDescription description = this.requestConverter.convert(request, metadata);
			// 项目生成器
			ProjectGenerator projectGenerator = new ProjectGenerator((
					projectGenerationContext) -> customizeProjectGenerationContext(projectGenerationContext, metadata));

			// 调用生成器方法生成项目（核心代码）
			ProjectGenerationResult result = projectGenerator.generate(description, generateProject(request));
			addTempFile(result.getRootDirectory(), result.getRootDirectory());

			// 在主干流程中，没有做任何写文件的操作（只创建了根文件夹）；
			// 它仅仅是定义了一套数据加载、扩展加载的机制与流程，
			// 将所有的具体实现都作为扩展的一部分。
			return result;
		}
		catch (ProjectGenerationException ex) {
			publishProjectFailedEvent(request, metadata, ex);
			throw ex;
		}
	}

	private ProjectAssetGenerator<ProjectGenerationResult> generateProject(R request) {
		return (context) -> {
			Path projectDir = new DefaultProjectAssetGenerator().generate(context);
			// 当一个新的项目被成功创建时发布 ProjectGeneratedEvent 这个事件
			publishProjectGeneratedEvent(request, context);
			// 返回项目生成的结果对象
			return new ProjectGenerationResult(context.getBean(ProjectDescription.class), projectDir);
		};
	}

	/**
	 * Invokes the project generation API that knows how to just write the build file.
	 * Returns a directory containing the project for the specified
	 * {@link ProjectRequest}.
	 * @param request the project request
	 * @return the generated build content
	 */
	public byte[] invokeBuildGeneration(R request) {
		InitializrMetadata metadata = this.parentApplicationContext.getBean(InitializrMetadataProvider.class).get();
		try {
			ProjectDescription description = this.requestConverter.convert(request, metadata);
			ProjectGenerator projectGenerator = new ProjectGenerator((
					projectGenerationContext) -> customizeProjectGenerationContext(projectGenerationContext, metadata));
			return projectGenerator.generate(description, generateBuild(request));
		}
		catch (ProjectGenerationException ex) {
			publishProjectFailedEvent(request, metadata, ex);
			throw ex;
		}
	}

	private ProjectAssetGenerator<byte[]> generateBuild(R request) {
		return (context) -> {
			byte[] content = generateBuild(context);
			publishProjectGeneratedEvent(request, context);
			return content;
		};
	}

	/**
	 * Create a file in the same directory as the given directory using the directory name
	 * and extension.
	 * @param dir the directory used to determine the path and name of the new file
	 * @param extension the extension to use for the new file
	 * @return the newly created file
	 */
	public Path createDistributionFile(Path dir, String extension) {
		Path download = dir.resolveSibling(dir.getFileName() + extension);
		addTempFile(dir, download);
		return download;
	}

	private void addTempFile(Path group, Path file) {
		this.temporaryFiles.computeIfAbsent(group, (key) -> new ArrayList<>()).add(file);
	}

	/**
	 * Clean all the temporary files that are related to this root directory.
	 * @param dir the directory to clean
	 * @see #createDistributionFile
	 */
	public void cleanTempFiles(Path dir) {
		List<Path> tempFiles = this.temporaryFiles.remove(dir);
		if (!tempFiles.isEmpty()) {
			tempFiles.forEach((path) -> {
				try {
					FileSystemUtils.deleteRecursively(path);
				}
				catch (IOException ex) {
					// Continue
				}
			});
		}
	}

	private byte[] generateBuild(ProjectGenerationContext context) throws IOException {
		ProjectDescription description = context.getBean(ProjectDescription.class);
		StringWriter out = new StringWriter();
		BuildWriter buildWriter = context.getBeanProvider(BuildWriter.class).getIfAvailable();
		if (buildWriter != null) {
			buildWriter.writeBuild(out);
			return out.toString().getBytes();
		}
		else {
			throw new IllegalStateException("No BuildWriter implementation found for " + description.getLanguage());
		}
	}

	private void customizeProjectGenerationContext(AnnotationConfigApplicationContext context,
			InitializrMetadata metadata) {

		// 通过 setParent 将应用的主上下文设置为这次 ProjectGenerationContext 的父节点。。
		context.setParent(this.parentApplicationContext);

		// 向 ProjectGenerationContext 中注册了元数据对象
		context.registerBean(InitializrMetadata.class, () -> metadata);
		context.registerBean(BuildItemResolver.class, () -> new MetadataBuildItemResolver(metadata,
				context.getBean(ProjectDescription.class).getPlatformVersion()));
		context.registerBean(MetadataProjectDescriptionCustomizer.class,
				() -> new MetadataProjectDescriptionCustomizer(metadata));
	}

	private void publishProjectGeneratedEvent(R request, ProjectGenerationContext context) {
		InitializrMetadata metadata = context.getBean(InitializrMetadata.class);
		ProjectGeneratedEvent event = new ProjectGeneratedEvent(request, metadata);
		this.eventPublisher.publishEvent(event);
	}

	private void publishProjectFailedEvent(R request, InitializrMetadata metadata, Exception cause) {
		ProjectFailedEvent event = new ProjectFailedEvent(request, metadata, cause);
		this.eventPublisher.publishEvent(event);
	}

}
